<template>
  <div class="treeItem">
    <div v-for="(item, index) in treeData" :key="item.id">
      <div class="child">
        <p
          @click="changeStaus(item, index)"
          :class="isOpen(item)"
          @contextmenu.prevent="showMenu($event, item, index)"
          draggable="true"
          @dragstart="dragStart($event, item, index)"
          @dragover="allowDrop($event)"
          @drop="drop($event, item, index)"
        >
          {{ item.name }}
        </p>
        <!-- 递归组件  必须要添加子组件发送的事件 并再次发送到父组件，不然事件会在父组件中监听不到 -->
        <tree-item
          @showMenu="showMenu"
          @change="change"
          v-if="item.isShow"
          :treeData="item.children"
        ></tree-item>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  name: "TreeItem",
  props: ["treeData"],
  components: {},
  data() {
    return {
      checkData: null, //选择的数据
      delDataParent: null, //需要删除的数据的父节点
      checkIndex: 0, //选中的节点
      checkIndex: 0, //右键时选中的节点
      openMultiterm: true, //是否可以展开多项
      isCanDrop: true, //是否可以移动节点
    };
  },
  computed: {
    // 使用计算属性动态的修改类名
    isOpen() {
      return function (item) {
        if (item.isShow && item.status) {
          // return "text-red";
          return "open";
        }
        if (item.check) {
          return "text-bule";
        }
        if (!item.children) {
          return "";
        }
        if (item.status && item.children) {
          if (item.children.length > 0) return "text";
        }
      };
    },
  },
  mounted() {
    this.getStatus();
  },
  watch: {
    treeData() {
      this.getStatus();
    },
  },
  methods: {
    // 向父节点发送节点变化的事件，
    // 注意：：：因为这里递归了组件 所以组件上面也需要绑定emit向上抛 不然在最大的父组件将监听不到
    // 及 层层上抛
    change(val) {
      this.$emit("change", val);
    },

    // 拖拽开始 可获得开始时的节点
    dragStart(e, item, index) {
      // console.log("拖拽开始", item, index);

      let data = item;
      data.index = index; //当前数据的下标
      // 不建议这样直接操作vuex的数据 这里只是为了便利
      this.$store.state.dragItem = data;
      // 当前选中的父数据
      this.$store.state.treeData = this.treeData;
    },

    // 阻止drag的默认处理事件 若无，则拖拽无效
    allowDrop(e) {
      // 阻止默认行为
      e.preventDefault();
    },

    // 判断拖动的节点是不是当前节点的子级节点
    getChild(arr, pid) {
      console.log('arr',arr);
      console.log('pid',pid);
      arr.map((item) => {
        if (item.pid === pid) {
          return (this.isCanDrop = false);
        }
        if (item.children) {
          this.getChild(item.children, pid);
        }
      });
    },

    // 拖拽结束 可获得结束时的节点
    drop(e, item, index) {
      // 从vuex中取出拖拽的节点数据
      let _data = this.$store.state.dragItem;
      let _treeData = this.$store.state.treeData;

      // 判断拖拽结束的节点是不是被拖拽的子节点，若是 则拦截
      // 此处存在问题
      if (_data.children) {
        this.isCanDrop = true;
        // 如果结束的节点没有子级 则将出现跨节点级的拖动时无法拖动
        // 但如果是不同级别的树节点 却又是可以操作的
        if (!item.children) {
          this.getChild(_data.children, _data.id);
        } else {
          this.getChild(_data.children, item.id);
        }
        if (!this.isCanDrop) return;
      }

      // 判断是不是移动到自己上面(及取消了移动) 如果是 则退出
      if (item.id === _data.id) return;

      // 判断移动的对象是不是在自己的父节点 若是 则退出
      if (item.children)
        if (item.children.some((item) => item.id === _data.id)) return;

      // 修改移动的元素的父节点id 修改为最新的父节点
      _data.pid = item.id;

      // 判断要移动到的位置是否拥有子节点 若无 则添加
      if (!item.children) {
        this.$set(item, "children", []);
      }

      // 将拖动的元素添加到结束元素的位置
      item.children.push(_data);

      // 删除拖动元素原来位置的数据
      Array.prototype.remove = function (index) {
        //prototype 给数组添加属性
        if (index > -1) {
          this.splice(index, 1); //利用splice()函数删除指定元素，splice() 方法用于插入、删除或替换数组的元素
        }
      };
      // 拿到需要删除的下标
      let _index = this.getDataInde(_treeData, _data.id);

      _treeData.remove(_index);

      // 监听一下子节点的状态 及 是否可以展开
      this.getStatus();
    },

    // 使用递归来寻找需要删除的节点下标
    getDataInde(arr, index) {
      for (let i = 0; i < arr.length; i++) {
        if (arr[i].children) this.getDataInde(arr[i].children, index);
        if (arr[i].id === index) return i;
      }
    },

    // 展示自定义的右键菜单
    showMenu(e, item, index) {
      // 选中的数据节点下标
      this.checkIndex = index;

      // 右键时 选中的数据
      this.checkData = item;

      // 将处理事件发送到父组件tree，处理函数任然 再本组件中完成
      // 发送出去主要是为了方便右键菜单的显示定位，将菜单组件防止本组件中 容易出问题 特别是浮动定位
      this.$emit("showMenu", e, item, index);
    },

    // 节点添加逻辑
    handleAdd(item) {
      // 若无子节点 则创建一个
      if (!item.children) {
        // 使用$set方法添加 不然实体不会刷新
        this.$set(item, "children", []);
      }
      this.addChild(item, item.id);
    },

    // 节点添加操作
    addChild(item, pid) {
      // 给一个随机的id
      let _id1 = (Math.random() * 10) >> 0;
      let _id2 = (Math.random() * 10) >> 0;
      let id = _id1 + "-" + _id2;

      item.children.push({
        pid,
        id,
        name: "new stuff" + id,
        isShow: false,
        check: false,
        status: false,
      });

      // 刷新一下状态
      this.getStatus();
    },

    // 删除节点
    handleDelete(item, index) {
      // 判断是否是最大的数据节点
      if (item.pid - 0 !== 0) {
        // 通过item的父id找到他所在的上一层数据位置 然后进行移除
        this.getItemParentByPid(this.treeData, item.pid);

        // 此时已获得父级数据 则开始删除
        this.delData(item.id);
      } else {
        // 为最大的父节点时，最大的父级数据就是treeData，所以这里可以直接删除
        this.treeData.splice(index, 1);
      }

      // 调用一下状态监测的函数 更新最新的页面状态
      this.getStatus();
    },

    // 根据对象的父节点id找到上一级数据
    getItemParentByPid(arr, pid) {
      arr.map((item) => {
        if (item.id === pid) {
          // 不知道为什么 数据没返回出去 所以我这里就将父节点的数据保存到变量中了
          return (this.delDataParent = item);
        }
        // 若有子级数据 则继续调用
        if (item.children) {
          this.getItemParentByPid(item.children, pid);
        }
      });
    },

    // 开始删除
    delData(id) {
      var li = this.delDataParent.children;
      var index;
      // 循环匹配数据 拿到下标
      for (var i = 0; i < li.length; i++) {
        if (li[i].id.indexOf(id) > -1) {
          index = i;
          li.splice(index, 1);
        }
      }
    },

    // 切换节点展开方式，是否可以多项展开
    handleUpdate() {
      this.openMultiterm = !this.openMultiterm;
    },

    // 修改节点树的展开状态
    // 性能不好
    itemStatus(arr, val) {
      arr.map((value) => {
        if (value.id !== val) this.$set(value, "isShow", false);
        if (value.children) {
          this.itemStatus(value.children, val);
        }
      });
    },

    // 判断展开、收起状态
    changeStaus(item, index) {
      // 将取消所有选择状态的逻辑抛到父组件处理
      this.$emit("change", item.id);

      // 设置选中的状态
      this.$set(item, "check", true);

      // 如果展开，就收起
      if (item.isShow == true) {
        this.$set(item, "isShow", false);
      } else {
        // 根据是否有children来判断是否能展开
        if (item.status) {
          this.$set(item, "isShow", true);

          // 关闭其他的展开项
          // 根据设置 判断是否需要关闭其他的选项
          if (!this.openMultiterm) {
            this.itemStatus(this.treeData, item.id);
          }
        }
      }
    },

    //判断是否有children
    getStatus() {
      if (this.treeData) {
        this.treeData.map((item, index) => {
          if ("children" in item) {
            // 不光要有子节点 还必须是存在数据才可以
            if (item.children) {
              if (item.children.length >= 1) {
                // 此时是正确拥有数据的
                this.$set(item, "status", true);
              } else {
                // 此时是存在子节点 但是没有数据，所以需要进行删除
                // 避免出现 没有下级目录 但是显示说还有下级目录的尴尬
                // 移除children 将值设置为未定义
                item.children = undefined;
                this.$set(item, "status", false);
              }
            }
          } else {
            this.$set(item, "status", false);
          }
        });
      }
    },
  },
};
</script>


<style>
p {
  margin-left: 20px;
}
.text-bule {
  color: blue;
}
.text-red {
  color: red;
}
.text-red:before {
  content: "-";
  position: absolute;
  left: 3px;
}
.open::before {
  content: "-";
  position: absolute;
  left: 3px;
}
.text:before {
  /* content: "+"; */
  content: "";
  width: 30px;
  height: 30px;
  background-size: 100%;
  background-image: url("https://ss0.bdstatic.com/70cFuHSh_Q1YnxGkpoWK1HF6hhy/it/u=786666821,3006585029&fm=11&gp=0.jpg");
  position: absolute;
  left: -15px;
}
.child {
  position: relative;
  left: 10px;
}

.contextmenu {
  margin: 0;
  height: 85px;
  width: 60px;
  background: #fff;
  z-index: 3000;
  position: absolute;
  list-style-type: none;
  padding: 5px 0;
  border-radius: 4px;
  font-size: 12px;
  font-weight: 400;
  color: #333;
  box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
}
</style>


